/*
 * Copyright (c) 2009-2012 David Grant
 * Copyright (c) 2010 ThruPoint Ltd
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.jscep.client;

import java.io.IOException;
import java.math.BigInteger;
import java.net.URL;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.cert.CertStore;
import java.security.cert.CertStoreException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Collection;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.x500.X500Principal;

import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.asn1.cms.IssuerAndSerialNumber;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.X509Extension;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.operator.ContentVerifierProvider;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.jscep.asn1.IssuerAndSubject;
import org.jscep.client.verification.CertificateVerifier;
import org.jscep.message.PkcsPkiEnvelopeDecoder;
import org.jscep.message.PkcsPkiEnvelopeEncoder;
import org.jscep.message.PkiMessageDecoder;
import org.jscep.message.PkiMessageEncoder;
import org.jscep.transaction.EnrollmentTransaction;
import org.jscep.transaction.MessageType;
import org.jscep.transaction.NonEnrollmentTransaction;
import org.jscep.transaction.OperationFailureException;
import org.jscep.transaction.Transaction;
import org.jscep.transaction.Transaction.State;
import org.jscep.transaction.TransactionException;
import org.jscep.transaction.TransactionId;
import org.jscep.transport.HttpGetTransport;
import org.jscep.transport.HttpPostTransport;
import org.jscep.transport.Transport;
import org.jscep.transport.TransportException;
import org.jscep.transport.request.GetCaCapsRequest;
import org.jscep.transport.request.GetCaCertRequest;
import org.jscep.transport.request.GetNextCaCertRequest;
import org.jscep.transport.response.Capabilities;
import org.jscep.transport.response.GetCaCapsResponseHandler;
import org.jscep.transport.response.GetCaCertResponseHandler;
import org.jscep.transport.response.GetNextCaCertResponseHandler;
import org.jscep.util.X500Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The <tt>Client</tt> class is used for interacting with a SCEP server.
 * <p>
 * Typical usage might look like so:
 * 
 * <pre>
 * // Create the client
 * URL server = new URL(&quot;http://jscep.org/scep/pkiclient.exe&quot;);
 * CertificateVerifier verifier = new ConsoleCertificateVerifier();
 * Client client = new Client(server, verifier);
 * 
 * // Invoke operations on the client.
 * client.getCaCapabilities();
 * </pre>
 * 
 * Each of the operations of this class is overloaded with a profile argument to
 * support SCEP servers with multiple (or mandatory) profile names.
 */
public final class Client {
	private static final Logger LOGGER = LoggerFactory.getLogger(Client.class);

	// A requester MUST have the following information locally configured:
	//
	// 1. The Certification Authority IP address or fully qualified domain name
	// 2. The Certification Authority HTTP CGI script path
	//
	// We use a URL for this.
	private final URL url;
	// A requester MUST have the following information locally configured:
	//
	// 3. The identifying information that is used for authentication of the
	// Certification Authority in Section 4.1.1. This information MAY be
	// obtained from the user, or presented to the end user for manual
	// authorization during the protocol exchange (e.g. the user indicates
	// acceptance of a fingerprint via a user-interface element).
	//
	// We use a callback handler for this.
	private final CallbackHandler handler;

	/**
	 * Constructs a new <tt>Client</tt> instance using the provided
	 * <tt>CallbackHandler</tt> for the provided URL.
	 * <p>
	 * The <tt>CallbackHandler</tt> must be able to handle
	 * {@link CertificateVerificationCallback}. Unless the
	 * <tt>CallbackHandler</tt> will be used to handle additional
	 * <tt>Callback</tt>s, users of this class are recommended to use the
	 * {@link #Client(URL, CertificateVerifier)} constructor instead.
	 * 
	 * @param url
	 *            the URL of the SCEP server.
	 * @param handler
	 *            the callback handler used to check the CA identity.
	 */
	public Client(URL url, CallbackHandler handler) {
		this.url = url;
		this.handler = handler;

		validateInput();
	}

	/**
	 * Constructs a new <tt>Client</tt> instance using the provided
	 * <tt>CertificateVerifier</tt> for the provided URL.
	 * <p/>
	 * The provided <tt>CertificateVerifier</tt> is used to verify that the
	 * identity of the SCEP server matches what the client expects.
	 * 
	 * @param url
	 *            the URL of the SCEP server.
	 * @param verifier
	 *            the verifier used to check the CA identity.
	 */
	public Client(URL url, CertificateVerifier verifier) {
		this.url = url;
		this.handler = new DefaultCallbackHandler(verifier);

		validateInput();
	}

	/**
	 * Validates all the input to this client.
	 * 
	 * @throws NullPointerException
	 *             if any member variables are null.
	 * @throws IllegalArgumentException
	 *             if any member variables are invalid.
	 */
	private void validateInput() throws NullPointerException,
			IllegalArgumentException {
		// Check for null values first.
		if (url == null) {
			throw new NullPointerException("URL should not be null");
		}
		if (!url.getProtocol().matches("^https?$")) {
			throw new IllegalArgumentException(
					"URL protocol should be HTTP or HTTPS");
		}
		if (url.getRef() != null) {
			throw new IllegalArgumentException(
					"URL should contain no reference");
		}
		if (url.getQuery() != null) {
			throw new IllegalArgumentException(
					"URL should contain no query string");
		}
		if (handler == null) {
			throw new NullPointerException(
					"Callback handler should not be null");
		}
	}

	// INFORMATIONAL REQUESTS

	/**
	 * Retrieves the set of SCEP capabilities from the CA.
	 * 
	 * @return the capabilities of the server.
	 */
	public Capabilities getCaCapabilities() {
		// NON-TRANSACTIONAL
		return getCaCapabilities(null);
	}

	/**
	 * Retrieves the capabilities of the SCEP server.
	 * <p>
	 * This method provides support for SCEP servers with multiple profiles.
	 * 
	 * @param profile
	 *            the SCEP server profile.
	 * @return the capabilities of the server.
	 */
	public Capabilities getCaCapabilities(final String profile) {
		LOGGER.debug("Determining capabilities of SCEP server");
		// NON-TRANSACTIONAL
		final GetCaCapsRequest req = new GetCaCapsRequest(profile);
		final Transport trans = new HttpGetTransport(url);
		try {
			return trans.sendRequest(req, new GetCaCapsResponseHandler());
		} catch (TransportException e) {
			LOGGER.warn("Transport problem when determining capabilities.  Using empty capabilities.");
			return new Capabilities();
		}
	}

	/**
	 * Retrieves the certificates used by the SCEP server.
	 * <p>
	 * This method queries the server for the certificates it will use in a SCEP
	 * message exchange. If the SCEP server represents a single entity, only a
	 * single CA certificate will be returned. If the SCEP server supports
	 * multiple entities (for example, if it uses a separate entity for signing
	 * SCEP messages), additional RA certificates will also be returned.
	 * 
	 * @return the certificate store.
	 * @throws ClientException
	 *             if any client error occurs.
	 * @see CertStoreInspector
	 */
	public CertStore getCaCertificate() throws ClientException {
		return getCaCertificate(null);
	}

	/**
	 * Retrieves the certificates used by the SCEP server.
	 * <p>
	 * This method queries the server for the certificates it will use in a SCEP
	 * message exchange. If the SCEP server represents a single entity, only a
	 * single CA certificate will be returned. If the SCEP server supports
	 * multiple entities (for example, if it uses a separate entity for signing
	 * SCEP messages), additional RA certificates will also be returned.
	 * <p>
	 * This method provides support for SCEP servers with multiple profiles.
	 * 
	 * @param profile
	 *            the SCEP server profile.
	 * @return the certificate store.
	 * @throws ClientException
	 *             if any client error occurs.
	 * @see CertStoreInspector
	 */
	public CertStore getCaCertificate(final String profile)
			throws ClientException {
		LOGGER.debug("Retrieving current CA certificate");
		// NON-TRANSACTIONAL
		// CA and RA public key distribution
		final GetCaCertRequest req = new GetCaCertRequest(profile);
		final Transport trans = new HttpGetTransport(url);

		CertStore store;
		try {
			store = trans.sendRequest(req, new GetCaCertResponseHandler());
		} catch (TransportException e) {
			throw new ClientException(e);
		}
		CertStoreInspector certs = CertStoreInspector.getInstance(store);
		verifyCA(certs.getIssuer());
		verifyRA(certs.getIssuer(), certs.getRecipient());
		verifyRA(certs.getIssuer(), certs.getSigner());

		return store;
	}

	private void verifyRA(X509Certificate ca, X509Certificate ra)
			throws ClientException {
		LOGGER.debug("Verifying signature of RA certificate");
		if (ca.equals(ra)) {
			LOGGER.debug("RA and CA are identical");

			return;
		}
		try {
			JcaX509CertificateHolder raHolder = new JcaX509CertificateHolder(ra);

			ContentVerifierProvider verifierProvider = new JcaContentVerifierProviderBuilder()
					.build(ca);

			if (!raHolder.isSignatureValid(verifierProvider)) {
				LOGGER.debug("Signature verification failed for RA.");
				throw new ClientException("RA not issued by CA");
			} else {
				LOGGER.debug("Signature verification passed for RA.");
			}
		} catch (Exception e) {
			throw new ClientException(e);
		}
	}

	/**
	 * Retrieves the next certificate to be used by the CA.
	 * <p>
	 * This method will query the SCEP server to determine if the CA is
	 * scheduled to start using a new certificate for issuing.
	 * 
	 * @return the certificate store.
	 * @throws ClientException
	 *             if any client error occurs.
	 * @see CertStoreInspector
	 */
	public CertStore getRolloverCertificate() throws ClientException {
		return getRolloverCertificate(null);
	}

	/**
	 * Retrieves the next certificate to be used by the CA.
	 * <p>
	 * This method will query the SCEP server to determine if the CA is
	 * scheduled to start using a new certificate for issuing.
	 * <p>
	 * This method provides support for SCEP servers with multiple profiles.
	 * 
	 * @param profile
	 *            the SCEP server profile.
	 * @return the certificate store.
	 * @throws ClientException
	 *             if any client error occurs.
	 * @see CertStoreInspector
	 */
	public CertStore getRolloverCertificate(final String profile)
			throws ClientException {
		LOGGER.debug("Retriving next CA certificate from CA");
		// NON-TRANSACTIONAL
		if (!getCaCapabilities(profile).isRolloverSupported()) {
			throw new UnsupportedOperationException();
		}
		final CertStore store = getCaCertificate(profile);
		// The CA or RA
		CertStoreInspector certs = CertStoreInspector.getInstance(store);
		final X509Certificate signer = certs.getSigner();

		final Transport trans = new HttpGetTransport(url);
		final GetNextCaCertRequest req = new GetNextCaCertRequest(profile);

		try {
			return trans.sendRequest(req, new GetNextCaCertResponseHandler(
					signer));
		} catch (TransportException e) {
			throw new ClientException(e);
		}
	}

	// TRANSACTIONAL

	/**
	 * Returns the certificate revocation list a given issuer and serial number.
	 * <p>
	 * This method requests a CRL for a certificate as identified by the issuer
	 * name and the certificate serial number.
	 * 
	 * @param identity
	 *            the identity of the client.
	 * @param key
	 *            the private key to sign the SCEP request.
	 * @param issuer
	 *            the name of the certificate issuer.
	 * @param serial
	 *            the serial number of the certificate.
	 * @return the CRL corresponding to the issuer and serial.
	 * @throws ClientException
	 *             if any client errors occurs.
	 * @throws OperationFailureException
	 *             if the request fails.
	 */
	public X509CRL getRevocationList(X509Certificate identity, PrivateKey key,
			final X500Principal issuer, final BigInteger serial)
			throws ClientException, OperationFailureException {
		return getRevocationList(identity, key, issuer, serial, null);
	}

	/**
	 * Returns the certificate revocation list a given issuer and serial number.
	 * <p>
	 * This method requests a CRL for a certificate as identified by the issuer
	 * name and the certificate serial number.
	 * <p>
	 * This method provides support for SCEP servers with multiple profiles.
	 * 
	 * @param identity
	 *            the identity of the client.
	 * @param key
	 *            the private key to sign the SCEP request.
	 * @param issuer
	 *            the name of the certificate issuer.
	 * @param serial
	 *            the serial number of the certificate.
	 * @param profile
	 *            the SCEP server profile.
	 * @return the CRL corresponding to the issuer and serial.
	 * @throws ClientException
	 *             if any client errors occurs.
	 * @throws OperationFailureException
	 *             if the request fails.
	 */
	@SuppressWarnings("unchecked")
	public X509CRL getRevocationList(X509Certificate identity, PrivateKey key,
			final X500Principal issuer, final BigInteger serial,
			final String profile) throws ClientException,
			OperationFailureException {
		LOGGER.debug("Retriving CRL from CA");
		// TRANSACTIONAL
		// CRL query
		checkDistributionPoints(profile);

		X500Name name = new X500Name(issuer.getName());
		IssuerAndSerialNumber iasn = new IssuerAndSerialNumber(name, serial);
		Transport transport = createTransport(profile);
		final Transaction t = new NonEnrollmentTransaction(transport,
				getEncoder(identity, key, profile), getDecoder(identity, key,
						profile), iasn, MessageType.GET_CRL);
		State state;
		try {
			state = t.send();
		} catch (TransactionException e) {
			throw new ClientException(e);
		}

		if (state == State.CERT_ISSUED) {
			try {
				Collection<X509CRL> crls = (Collection<X509CRL>) t
						.getCertStore().getCRLs(null);
				if (crls.size() == 0) {
					return null;
				}
				return crls.iterator().next();
			} catch (CertStoreException e) {
				throw new RuntimeException(e);
			}
		} else if (state == State.CERT_REQ_PENDING) {
			throw new IllegalStateException();
		} else {
			throw new OperationFailureException(t.getFailInfo());
		}
	}

	private void checkDistributionPoints(final String profile)
			throws ClientException {
		CertStore store = getCaCertificate(profile);
		CertStoreInspector certs = CertStoreInspector.getInstance(store);
		final X509Certificate ca = certs.getIssuer();
		if (ca.getExtensionValue(X509Extension.cRLDistributionPoints.getId()) != null) {
			LOGGER.warn("CA supports distribution points");
		}
	}

	/**
	 * Retrieves the certificate corresponding to the provided serial number.
	 * <p>
	 * This request relates only to the current CA certificate. If the CA
	 * certificate has changed since the requested certificate was issued, this
	 * operation will fail.
	 * 
	 * @param identity
	 *            the identity of the client.
	 * @param key
	 *            the private key to sign the SCEP request.
	 * @param serial
	 *            the serial number of the requested certificate.
	 * @return the certificate store containing the requested certificate.
	 * @throws ClientException
	 *             if any client error occurs.
	 * @throws OperationFailureException
	 *             if the SCEP server refuses to service the request.
	 */
	public CertStore getCertificate(X509Certificate identity, PrivateKey key,
			BigInteger serial) throws ClientException,
			OperationFailureException {
		return getCertificate(identity, key, serial, null);
	}

	/**
	 * Retrieves the certificate corresponding to the provided serial number.
	 * <p>
	 * This request relates only to the current CA certificate. If the CA
	 * certificate has changed since the requested certificate was issued, this
	 * operation will fail.
	 * <p>
	 * This method provides support for SCEP servers with multiple profiles.
	 * 
	 * @param identity
	 *            the identity of the client.
	 * @param key
	 *            the private key to sign the SCEP request.
	 * @param serial
	 *            the serial number of the requested certificate.
	 * @param profile
	 *            the SCEP server profile.
	 * @return the certificate store containing the requested certificate.
	 * @throws ClientException
	 *             if any client error occurs.
	 * @throws OperationFailureException
	 *             if the SCEP server refuses to service the request.
	 */
	public CertStore getCertificate(X509Certificate identity, PrivateKey key,
			BigInteger serial, String profile)
			throws OperationFailureException, ClientException {
		LOGGER.debug("Retriving certificate from CA");
		// TRANSACTIONAL
		// Certificate query
		final CertStore store = getCaCertificate(profile);
		CertStoreInspector certs = CertStoreInspector.getInstance(store);
		final X509Certificate ca = certs.getIssuer();

		X500Name name = new X500Name(ca.getIssuerX500Principal().toString());
		IssuerAndSerialNumber iasn = new IssuerAndSerialNumber(name, serial);
		Transport transport = createTransport(profile);
		final Transaction t = new NonEnrollmentTransaction(transport,
				getEncoder(identity, key, profile), getDecoder(identity, key,
						profile), iasn, MessageType.GET_CERT);

		State state;
		try {
			state = t.send();
		} catch (TransactionException e) {
			throw new ClientException(e);
		}

		if (state == State.CERT_ISSUED) {
			return t.getCertStore();
		} else if (state == State.CERT_REQ_PENDING) {
			throw new IllegalStateException();
		} else {
			throw new OperationFailureException(t.getFailInfo());
		}
	}

	/**
	 * Sends a CSR to the SCEP server for enrolling in a PKI.
	 * <p>
	 * This method enrols the provider <tt>CertificationRequest</tt> into the
	 * PKI represented by the SCEP server.
	 * 
	 * @param identity
	 *            the identity of the client.
	 * @param key
	 *            the private key to sign the SCEP request.
	 * @param csr
	 *            the CSR to enrol.
	 * @return the certificate store returned by the server.
	 * @throws ClientException
	 *             if any client error occurs.
	 * @throws OperationFailureException
	 *             if the operation fails.
	 * @throws PollingTerminatedException
	 *             if polling is terminated
	 * @throws TransactionException
	 *             if there is a problem with the SCEP transaction.
	 * @see CertStoreInspector
	 */
	public EnrollmentResponse enrol(X509Certificate identity, PrivateKey key,
			final PKCS10CertificationRequest csr) throws ClientException,
			TransactionException {
		return enrol(identity, key, csr, null);
	}

	/**
	 * Sends a CSR to the SCEP server for enrolling in a PKI.
	 * <p>
	 * This method enrols the provider <tt>CertificationRequest</tt> into the
	 * PKI represented by the SCEP server.
	 * 
	 * @param identity
	 *            the identity of the client.
	 * @param key
	 *            the private key to sign the SCEP request.
	 * @param csr
	 *            the CSR to enrol.
	 * @param profile
	 *            the SCEP server profile.
	 * @return the certificate store returned by the server.
	 * @throws ClientException
	 *             if any client error occurs.
	 * @throws OperationFailureException
	 *             if the operation fails.
	 * @throws PollingTerminatedException
	 *             if polling is terminated
	 * @throws TransactionException
	 *             if there is a problem with the SCEP transaction.
	 * @see CertStoreInspector
	 */
	public EnrollmentResponse enrol(X509Certificate identity, PrivateKey key,
			final PKCS10CertificationRequest csr, String profile)
			throws ClientException, TransactionException {
		LOGGER.debug("Enrolling certificate with CA");

		if (isSelfSigned(identity)) {
			LOGGER.debug("Certificate is self-signed");
			X500Name csrSubject = csr.getSubject();
			X500Name idSubject = X500Utils.toX500Name(identity
					.getSubjectX500Principal());

			if (!csrSubject.equals(idSubject)) {
				LOGGER.error("The self-signed certificate MUST use the same subject name as in the PKCS#10 request.");
			}
		}
		// TRANSACTIONAL
		// Certificate enrollment
		final Transport transport = createTransport(profile);
		PkiMessageEncoder encoder = getEncoder(identity, key, profile);
		PkiMessageDecoder decoder = getDecoder(identity, key, profile);
		final EnrollmentTransaction trans = new EnrollmentTransaction(
				transport, encoder, decoder, csr);

		try {
			MessageDigest digest = getCaCapabilities(profile)
					.getStrongestMessageDigest();
			byte[] hash = digest.digest(csr.getEncoded());

			LOGGER.info("{} PKCS#10 Fingerprint: [{}]", digest.getAlgorithm(),
					Hex.encodeHexString(hash));
		} catch (IOException e) {
			LOGGER.error("Error getting encoded CSR", e);
		}

		return send(trans);
	}

	private boolean isSelfSigned(X509Certificate cert) throws ClientException {
		try {
			JcaX509CertificateHolder holder = new JcaX509CertificateHolder(cert);
			ContentVerifierProvider verifierProvider = new JcaContentVerifierProviderBuilder()
					.build(holder);

			return holder.isSignatureValid(verifierProvider);
		} catch (Exception e) {
			throw new ClientException(e);
		}
	}

	public EnrollmentResponse poll(X509Certificate identity,
			PrivateKey identityKey, X500Principal subject, TransactionId transId)
			throws ClientException, TransactionException {
		return poll(identity, identityKey, subject, transId, null);
	}

	public EnrollmentResponse poll(X509Certificate identity,
			PrivateKey identityKey, X500Principal subject,
			TransactionId transId, String profile) throws ClientException,
			TransactionException {
		final Transport transport = createTransport(profile);
		CertStore store = getCaCertificate(profile);
		CertStoreInspector certStore = CertStoreInspector.getInstance(store);
		X509Certificate issuer = certStore.getIssuer();

		PkiMessageEncoder encoder = getEncoder(identity, identityKey, profile);
		PkiMessageDecoder decoder = getDecoder(identity, identityKey, profile);

		IssuerAndSubject ias = new IssuerAndSubject(X500Utils.toX500Name(issuer
				.getIssuerX500Principal()), X500Utils.toX500Name(subject));

		final EnrollmentTransaction trans = new EnrollmentTransaction(
				transport, encoder, decoder, ias, transId);
		return send(trans);
	}

	private EnrollmentResponse send(final EnrollmentTransaction trans)
			throws TransactionException {
		State s = trans.send();

		if (s == State.CERT_ISSUED) {
			return new EnrollmentResponse(trans.getId(), trans.getCertStore());
		} else if (s == State.CERT_REQ_PENDING) {
			return new EnrollmentResponse(trans.getId());
		} else {
			return new EnrollmentResponse(trans.getId(), trans.getFailInfo());
		}
	}

	private PkiMessageEncoder getEncoder(X509Certificate identity,
			PrivateKey priKey, String profile) throws ClientException {
		CertStore store = getCaCertificate(profile);
		Capabilities caps = getCaCapabilities(profile);
		CertStoreInspector certs = CertStoreInspector.getInstance(store);
		X509Certificate recipientCertificate = certs.getRecipient();
		PkcsPkiEnvelopeEncoder envEncoder = new PkcsPkiEnvelopeEncoder(
				recipientCertificate, caps.getStrongestCipher());

		String sigAlg = caps.getStrongestSignatureAlgorithm();
		return new PkiMessageEncoder(priKey, identity, envEncoder, sigAlg);
	}

	private PkiMessageDecoder getDecoder(X509Certificate identity,
			PrivateKey key, String profile) throws ClientException {
		final CertStore store = getCaCertificate(profile);
		CertStoreInspector certs = CertStoreInspector.getInstance(store);
		X509Certificate signer = certs.getSigner();
		PkcsPkiEnvelopeDecoder envDecoder = new PkcsPkiEnvelopeDecoder(
				identity, key);

		return new PkiMessageDecoder(signer, envDecoder);
	}

	/**
	 * Creates a new transport based on the capabilities of the server.
	 * 
	 * @param profile
	 *            profile to use for determining if HTTP POST is supported
	 * @return the new transport.
	 * @throws IOException
	 *             if any I/O error occurs.
	 */
	private Transport createTransport(final String profile) {
		if (getCaCapabilities(profile).isPostSupported()) {
			return new HttpPostTransport(url);
		} else {
			return new HttpGetTransport(url);
		}
	}

	private void verifyCA(X509Certificate cert) throws ClientException {
		CertificateVerificationCallback callback = new CertificateVerificationCallback(
				cert);
		try {
			LOGGER.debug("Requesting certificate verification.");
			Callback[] callbacks = new Callback[] { callback };
			handler.handle(callbacks);
		} catch (UnsupportedCallbackException e) {
			LOGGER.debug("Certificate verification failed.");
			throw new ClientException(e);
		} catch (IOException e) {
			throw new ClientException(e);
		}
		if (!callback.isVerified()) {
			LOGGER.debug("Certificate verification failed.");
			throw new ClientException(
					"CA certificate fingerprint could not be verified.");
		} else {
			LOGGER.debug("Certificate verification passed.");
		}
	}
}
